using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Coditate.Common.Util;
using Coditate.Savant.Formatters;
using Coditate.Savant.TestSupport;
using NUnit.Framework;
using AmazonAttribute=Amazon.SimpleDB.Model.Attribute;

namespace Coditate.Savant.Core
{
    [TestFixture]
    public class MappingUtilsTest
    {
        private PropertyFormatter formatter;
        private TypeItemMapping mapping;

        [SetUp]
        public void SetUp()
        {
            mapping = TypeItemMapping.GetMapping(typeof (TestItem));
            formatter = new PropertyFormatter(new SavantConfig());
        }

        [Test]
        public void AddListPropertyValue_NullValue()
        {
            TypeItemMapping itemMapping = TypeItemMapping.GetMapping(typeof (C));
            var values = new PropertyValues(Guid.NewGuid());
            values["EmptyListValue"] = new List<int>();

            MappingUtils.AddListPropertyValue(itemMapping["EmptyListValue"], values, null);

            Assert.IsEmpty((ICollection) values["EmptyListValue"]);
        }

        [Test]
        public void SplitJoin_Random()
        {
            SplitJoinRandom(false, .3, 10);
        }

        [Test]
        public void SplitJoin_RandomCompressed()
        {
            SplitJoinRandom(true, .15, 10);
        }

        private void SplitJoinRandom(bool compress, double fillPercent, int runs)
        {
            for (int k = 0; k < runs; k++)
            {
                int maxAttributeLength = RandomData.Generator.Next(10, 1500);
                // encoding of random data results in more than 1 byte per character so limit size to 40% of byte capacity
                var dataLength =
                    (int)
                    ((maxAttributeLength - MappingUtils.ChunkIndexLength)*
                     RandomData.Generator.Next(0, MappingUtils.MaxChunks)*fillPercent);
                string data1 = RandomData.String(dataLength);
                List<string> chunks = MappingUtils.SplitPropertyValue(data1, maxAttributeLength, compress);
                string data2 = MappingUtils.JoinAttributeValues(chunks, maxAttributeLength, compress);
                Assert.AreEqual(data1, data2);

                // verify that UTF-8 encoded characters don't exceed max allowed size
                foreach (string s in chunks)
                {
                    byte[] b = Encoding.UTF8.GetBytes(s);
                    Assert.GreaterOrEqual(maxAttributeLength, b.Length);
                }
            }
        }

        [Test]
        public void SplitJoin_Boundaries()
        {
            int maxAttributeLength = 10;
            // dataLength is intentionally the largest prime number under 7000
            int dataLength = 6997;
            string data1 = RandomData.AlphaNumericString(dataLength, true);
            int testCount = 100;
            for (int k = 0; k < testCount; k++)
            {
                List<string> chunks = MappingUtils.SplitPropertyValue(data1, maxAttributeLength, false);
                string data2 = MappingUtils.JoinAttributeValues(chunks, maxAttributeLength, false);
                Assert.AreEqual(data1, data2);
                maxAttributeLength++;
            }
        }

        [Test,
         ExpectedException(typeof (SavantDataException),
             ExpectedMessage =
                 "Spanned property overflow. String property with length of 7001 characters requires more than 1000 attributes to store."
             )]
        public void SplitPropertyValue_Overflow()
        {
            int maxAttributeSize = 10;
            int overflowLength = (maxAttributeSize - MappingUtils.ChunkIndexLength)*MappingUtils.MaxChunks + 1;
            string value = RandomData.AlphaNumericString(overflowLength, true);

            MappingUtils.SplitPropertyValue(value, 10, false);
        }

        [Test,
         ExpectedException(typeof (SavantConfigurationException),
             ExpectedMessage =
                 "SavantConfig.MaxAttributeLength may not be set to less than 4"
             )]
        public void SplitPropertyValue_InvalidAttributeLength()
        {
            MappingUtils.SplitPropertyValue("", 0, false);
        }

        [Test,
         ExpectedException(typeof (SavantConfigurationException),
             ExpectedMessage =
                 "SavantConfig.MaxAttributeLength may not be set to less than 4"
             )]
        public void JoinAttributeValues_InvalidAttributeLength()
        {
            MappingUtils.JoinAttributeValues(new List<string> {"1234"}, 0, false);
        }

        [Test,
         ExpectedException(typeof (SavantDataException),
             ExpectedMessage =
                 "Unable to reassemble spanned attributes property. The data may have been corrupted."
             )]
        public void JoinAttributeValues_CorruptedData()
        {
            MappingUtils.JoinAttributeValues(new List<string> {"1"}, 10, false);
        }

        [Test,
         ExpectedException(typeof (SavantDataException),
             ExpectedMessage =
                 "Unable to instantiate item of type 'Coditate.Savant.Core.MappingUtilsTest+NoDefaultConstructorItem'. Does the type have a public no-arg constructor?"
             )]
        public void CreateInstance_Error()
        {
            MappingUtils.CreateInstance(typeof (NoDefaultConstructorItem));
        }

        [Test,
         ExpectedException(typeof (SavantDataException),
             ExpectedMessage =
                 "Item name of item 'Coditate.Savant.Core.MappingUtilsTest+TestItem' is null or an empty string."
             )]
        public void GetPropertyValues_NullItemName()
        {
            var g = new TestItem();
            MappingUtils.GetPropertyValues(TypeItemMapping.GetMapping(typeof (TestItem)), g);
        }

        [Test,
         ExpectedException(typeof (SavantDataException),
             ExpectedMessage =
                 "Item name of type 'System.Boolean' could not be converted to a string using the formatter configured for item name property 'Coditate.Savant.Core.MappingUtilsTest+TestItem.ItemName'."
             )]
        public void ItemNameToString_Error()
        {
            MappingUtils.ItemNameToString(formatter, mapping.ItemNameMapping, true);
        }

        [Test,
         ExpectedException(typeof (SavantDataException),
             ExpectedMessage =
                 "Property 'Coditate.Savant.Core.MappingUtilsTest+TestItem.IntValue' with value 'True' could not be converted to a string. The property type is 'System.Int32'."
             )]
        public void PropertyValueToString_Error()
        {
            AttributeMapping intValueMapping =
                mapping.AttributeMappings.Where(p => p.AttributeName == "IntValue").FirstOrDefault();
            MappingUtils.PropertyValueToString(formatter, intValueMapping, true);
        }

        [Test,
         ExpectedException(typeof (SavantDataException),
             ExpectedMessage =
                 "Error adding value to list property 'Coditate.Savant.Core.MappingUtilsTest+TestItem.AlwaysErrorListValue'. The property type is 'System.Collections.Generic.List`1[System.Int32]'. The value type is 'System.String'."
             )]
        public void AddListProperty_Error()
        {
            var values = new PropertyValues(Guid.NewGuid());
            values["AlwaysErrorListValue"] = new List<int>();
            AttributeMapping errorValueMapping =
                mapping.AttributeMappings.Where(p => p.AttributeName == "AlwaysErrorListValue").FirstOrDefault();
            MappingUtils.AddListPropertyValue(errorValueMapping, values, "");
        }

        [Test]
        public void SetPropertyValues_ExtraValuesIgnored()
        {
            var values = new PropertyValues(0L);
            values["InvalidProperty"] = "My test property value";
            values["IntValue"] = RandomData.Generator.Next();

            var g = new TestItem();
            MappingUtils.SetPropertyValues(TypeItemMapping.GetMapping(typeof (TestItem)), values, g);

            Assert.AreEqual(g.IntValue, values["IntValue"]);
        }

        [Test,
         ExpectedException(typeof (SavantDataException),
             ExpectedMessage =
                 "String value 'True' could not be converted to expected property type 'System.Int32' for property 'Coditate.Savant.Core.MappingUtilsTest+TestItem.IntValue'."
             )]
        public void StringToPropertyValue_Error()
        {
            AttributeMapping intValueMapping =
                mapping.AttributeMappings.Where(p => p.AttributeName == "IntValue").FirstOrDefault();
            MappingUtils.StringToPropertyValue(formatter, intValueMapping, "True");
        }

        [Test,
         ExpectedException(typeof (SavantDataException),
             ExpectedMessage =
                 "Item type 'System.Object' has no public, read/write property named 'abc'"
             )]
        public void GetPropertyValue_InvalidName()
        {
            AttributeMapping mapping = AttributeMapping.Create("abc", typeof (int));
            MappingUtils.GetPropertyValue(new object(), mapping);
        }

        [Test,
         ExpectedException(typeof (SavantDataException),
             ExpectedMessage =
                 "Error reading property 'Coditate.Savant.Core.MappingUtilsTest+AlwaysErrorPropertyItem.IntValue'. The property type is 'System.Int32'."
             )]
        public void GetPropertyValue_InvalidValue()
        {
            AttributeMapping mapping = new TypeAttributeMapping(typeof (AlwaysErrorPropertyItem).GetProperty("IntValue"));
            MappingUtils.GetPropertyValue(new AlwaysErrorPropertyItem(), mapping);
        }

        [Test,
         ExpectedException(typeof (SavantDataException),
             ExpectedMessage =
                 "Error setting property 'Coditate.Savant.Core.MappingUtilsTest+AlwaysErrorPropertyItem.IntValue'. The declared property type is 'System.Int32'. The actual value type is 'System.String'."
             )]
        public void SetPropertyValue_InvalidValue()
        {
            AttributeMapping mapping = new TypeAttributeMapping(typeof (AlwaysErrorPropertyItem).GetProperty("IntValue"));
            MappingUtils.SetPropertyValue(new AlwaysErrorPropertyItem(), mapping, "");
        }

        public class AlwaysErrorPropertyItem : TestItemBase
        {
            public int IntValue
            {
                get { throw new Exception(); }
                set { throw new Exception(); }
            }
        }

        public class NoDefaultConstructorItem : TestItemBase
        {
            public NoDefaultConstructorItem(int value)
            {
            }
        }

        public class TestItem
        {
            [NumberFormat(1, 1, false)]
            [ItemName]
            public long? ItemName { get; set; }

            [NumberFormat(1, 1, false)]
            public int IntValue { get; set; }

            public int AlwaysErrorIntValue
            {
                set { throw new Exception(); }
                get { throw new Exception(); }
            }

            public List<int> AlwaysErrorListValue
            {
                set { throw new Exception(); }
                get { throw new Exception(); }
            }
        }
    }
}